Skip to content

Bind only $x, not x in def f($x): ...#3520

Open
01mf02 wants to merge 1 commit intojqlang:masterfrom
01mf02:regular-bind-less
Open

Bind only $x, not x in def f($x): ...#3520
01mf02 wants to merge 1 commit intojqlang:masterfrom
01mf02:regular-bind-less

Conversation

@01mf02
Copy link
Copy Markdown
Contributor

@01mf02 01mf02 commented Apr 14, 2026

@wader brought to my attention in 01mf02/jaq#428 that jq currently has the following behaviour:

$ jq -cn 'def f($a): [a],$a; f(1,2)'
[1,2]
1
[1,2]
2

That means that in a function f($a), both a and $a are bound. This has already been pointed out by @pkoppstein in @nicowilliams's PR #524, which added "regular" definition arguments (where "regular" means "$..." here).

This PR tries to rectify this, by changing how to compile regular definition arguments. Let us use @wader's example above and see what it rewrites to:

# before
def f(x0): x0 as a | a as $a | [a], $a;
# after
def f(x0): x0 as @regular | @regular as $a | [a], $a;

The only change is that it binds any regular argument as @regular, which is a variable name that cannot be referenced by the user.

This excludes a few programs that previously compiled (including @wader's example above):

# before
$ jq -n 'def f($a): a; f(1)'
1
# after
$ jq -n 'def f($a): a; f(1)'
jq: error: a/0 is not defined at <top-level>, line 1, column 12:
    def f($a): a; f(1)
               ^
jq: 1 compile error

Furthermore, it changes the meaning of some programs:

# before
$ jq -nc 'def foo(a;$a):[a,$a]; foo(1;2)'
[2,2]
# after
$ jq -nc 'def foo(a;$a):[a,$a]; foo(1;2)'
[1,2]

@01mf02 01mf02 changed the title Bind only $x, not x in def f($x): .... Bind only $x, not x in def f($x): ... Apr 14, 2026
@01mf02
Copy link
Copy Markdown
Contributor Author

01mf02 commented Apr 14, 2026

By the way, a little note on LLM usage: I tested whether LLMs could be useful to do this fix. I gave ChatGPT the following prompts:

Write a patch for jq that prevents binding variable arguments additionally as regular arguments. That is, today def f($x): x compiles, because it binds its argument both as x and $x. However, with the patch, this should yield an error, while def f($x): $x should continue to work.

It gave me a completely unusable patch, then told me: "If you want, I can refine this into a PR-ready patch against a specific jq version (with exact line numbers and tests).". I replied:

Refine this into a PR-ready patch against jq master.

Behold its result, headlined boldly by "✅ Patch (ready to submit)":

diff --git a/src/compile.c b/src/compile.c
index 7c1e5ab..e4d9abc 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -1308,18 +1308,35 @@ static int compile_funcdef(block* b, struct locfile* lf, jv name, jv args, block
   int nformals = jv_array_length(jv_copy(args));

   for (int i = 0; i < nformals; i++) {
-    jv arg = jv_array_get(jv_copy(args), i);
-    const char* argname = jv_string_value(arg);
-
-    // Existing behavior: bind both positional and variable
-    block_bind_argument(b, argname);
-    block_bind_variable(b, argname);
+    jv arg = jv_array_get(jv_copy(args), i);
+    const char* raw = jv_string_value(arg);
+
+    if (raw[0] == '$') {
+      // Variable argument: strip leading '$'
+      const char* varname = raw + 1;
+
+      if (varname[0] == '\0') {
+        locfile_locate(lf, jv_copy(arg));
+        FAIL(*b, "invalid variable name \"$\"");
+      }
+
+      // Bind ONLY as variable
+      block_bind_variable(b, varname);
+
+      // NOTE: intentionally NOT calling block_bind_argument()
+    } else {
+      // Positional argument: preserve existing semantics
+      block_bind_argument(b, raw);
+      block_bind_variable(b, raw);
+    }
   }

   jv_free(args);

   // Compile function body
   block body_block = gen_noop();
   int r = compile(b, lf, body, &body_block);

   if (r != 0)
     return r;

Hallucination at its finest. The file exists, but neither does the function compile_funcdef exist, nor most of the other things in this diff. So this is complete AI slop.

... However, it brought me on track with the nformals reference, which is actually used at the place that I modified in the end. So while using the LLM on its own is completely insufficient, it seems that this can still be useful to discover likely places to modify.

In this case, however, it would have been fastest to just look up the commit that added the "regular argument" functionality (32c08d4). Which is what I ended up doing anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant